{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id=\"tutorial\"></a>\n",
    "# Load flow calculation with distributed slack"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In power flow analysis, a slack bus is chosen which compensates the mismatch of active power in the grid as related to the balance of consumption, generation and losses. This mismatch results from grid losses or an unbalanced dispatch. In real transmission and distribution systems, multiple generators can play a role in compensating the mismatch of active power. The approach of _distributed slack_ allows to model the allocation of the mismatch compensation to a number of buses according to pre-defined weights, which more closely resembles real-world scenarios. To describe how much active power a single element contributes to the compensation of the mismatch of active power, the parameter *slack_weight* is used. \n",
    "\n",
    "In pandapower, the load flow calculation with Newton-Raphson algorithm supports the functionality of distributed slack. To this end, the user can set the argument *distributed_slack* to True when calling the function runpp. The slack weights for *ext_grid*, *gen* and *xward* elements should be provided in the *slack_weight* column."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### How the slack weight is considered\n",
    "The sum of all slack weights of the contributing elements should be 1 and must be greater than 0. If the sum of the slack weights is other than 1, it is normalized to 1 before the load flow calculation. If the net has exactly one slack, e.g. an ext_grid, and only this element has a slack_weight other than 0, a load flow calculation considering distributed slack will have the same results as a load flow calculation without using the distributed slack (details in the first example below)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='power_flow'></a>\n",
    "### How the active power mismatch is attributed to the elements\n",
    "The resulting active power of an element $i$ after the load flow calculation with distributed slack can be represented as follows ($ng$: number of generation plants, $nc$: number of consumptions, $nb$: number of branches):\n",
    "\n",
    "<a id=\"sign\"></a>\n",
    "$$P_{result,i}= P_{set,i} - (\\sum_{elm=1}^{ng}P_{generation,elm} - \\sum_{elm=1}^{nc}P_{consumption,elm} - \\sum_{branch=1}^{nb}P_{losses,branch}) \\cdot slack\\_weight_i$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Calculation example\n",
    "We import necessary modules and create an example grid model:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T13:19:39.582633Z",
     "start_time": "2025-10-17T13:19:35.486928Z"
    }
   },
   "outputs": [],
   "source": [
    "# configures the rendering and features of plots which use matplotlib:\n",
    "import matplotlib\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib notebook\n",
    "\n",
    "from copy import deepcopy\n",
    "from pandapower.plotting.simple_plot import simple_plot\n",
    "from pandapower.networks.power_system_test_cases import case9\n",
    "from pandapower.run import runpp\n",
    "\n",
    "from IPython.display import display, Markdown\n",
    "\n",
    "heading_dist_slack = Markdown(\"#### With distributed slack\")\n",
    "heading_no_dist_slack = Markdown(\"#### Without distributed slack\")\n",
    "\n",
    "net = case9()\n",
    "fig = plt.figure(figsize=(4,4))\n",
    "ax = fig.add_subplot()\n",
    "simple_plot(net, plot_loads=True, plot_gens=True, load_size=2.5, gen_size=0.1, ax=ax);\n",
    "fig.tight_layout(pad=0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id=\"first_case\"></a>\n",
    "## First example: distributed slack with just the ext_grid element\n",
    "In each case, we first define the contributing elements and their slack weights. Then we compare the results of the load flow calculation with and without the distributed slack calculation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T13:19:39.597853Z",
     "start_time": "2025-10-17T13:19:39.589828Z"
    }
   },
   "outputs": [],
   "source": [
    "def print_powers(net):\n",
    "    l1 = [\"Consumption of loads: \", f\"{net.load.at[0, 'p_mw']:>6.2f} MW\\t{net.load.at[1, 'p_mw']:>6.2f} MW\\t{net.load.at[2, 'p_mw']:>6.2f} MW\\t({net.load.p_mw.sum():>6.2f} MW total)\"]\n",
    "    l2 = [\"Injection of generators (input): \", f\"{net.gen.at[0, 'p_mw']:>6.2f} MW\\t{net.gen.at[1, 'p_mw']:>6.2f} MW\\t{'':9}\\t({net.gen.p_mw.sum():>6.2f} MW total)\"]\n",
    "    l3 = [\"Injection of generators (results): \", f\"{net.res_gen.at[0, 'p_mw']:>6.2f} MW\\t{net.res_gen.at[1, 'p_mw']:>6.2f} MW\\t{'':9}\\t({net.res_gen.p_mw.sum():>6.2f} MW total)\"]\n",
    "    l4 = [\"Injection of generators (difference): \", f\"{net.res_gen.at[0, 'p_mw']-net.gen.at[0, 'p_mw']:>6.2f} MW\\t{net.res_gen.at[1, 'p_mw']-net.gen.at[1, 'p_mw']:>6.2f} MW\\t{'':9}\\t({net.res_gen.p_mw.sum()-net.gen.p_mw.sum():>6.2f} MW total)\"]\n",
    "    l5 = [\"Slack power at ext_grid (results): \", f\"{net.res_ext_grid.at[0, 'p_mw']:>6.2f} MW\"]\n",
    "    l6 = [\"Line losses (results): \", f\"{net.res_line.pl_mw.sum():>6.2f} MW\"]\n",
    "    for l in [l1, l2, l3, l4, l5, l6]:\n",
    "        print(f\"{l[0]:38}\\t{l[1]}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T13:19:41.451247Z",
     "start_time": "2025-10-17T13:19:39.603826Z"
    }
   },
   "outputs": [],
   "source": [
    "display(heading_no_dist_slack)\n",
    "runpp(net)\n",
    "\n",
    "print_powers(net)\n",
    "\n",
    "display(heading_dist_slack)\n",
    "# set slack_weight of the relevant elements\n",
    "net.ext_grid['slack_weight'] = 1\n",
    "net.gen['slack_weight'] = 0\n",
    "\n",
    "runpp(net, distributed_slack=True)\n",
    "\n",
    "print_powers(net)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As expected, the results of the calculation with distributed slack are equivalent to the results of the load flow calculation without distributed slack."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='second_case'></a>\n",
    "## Second example: equal slack weights for all elements"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T13:19:41.606996Z",
     "start_time": "2025-10-17T13:19:41.503477Z"
    }
   },
   "outputs": [],
   "source": [
    "display(heading_no_dist_slack)\n",
    "runpp(net)\n",
    "\n",
    "print_powers(net)\n",
    "\n",
    "display(heading_dist_slack)\n",
    "net.ext_grid['slack_weight'] = 1\n",
    "net.gen['slack_weight'] = 1\n",
    "\n",
    "runpp(net, distributed_slack=True)\n",
    "\n",
    "print_powers(net)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Sanity check\n",
    "\n",
    "In this case, the slack power is equally distributed among the ext_grid and gen elements. The power mismatch can be calculated by subtracting the consumption and line losses from the setpoint for injection: \n",
    "\n",
    "$$248 MW - 315 MW - 7.78 MW = -74.78 MW$$\n",
    "\n",
    "The power mismatch is negative, which means that $74.78 MW$ are missing for the active power to be balanced. This amount is added to the distributed slack elements ext_grid and gen according to their slack weights, resulting in the difference to the setpoints of $24.93 MW$ each (ext_grid is assumed to have a setpoint of 0)\n",
    "\n",
    "The results can be further checked by using the following calculation:\n",
    "$$P_{\\text{dist\\_slack}}=\\left(P_{\\text{res\\_ext\\_grid}}-P_{\\text{ext\\_grid}}\\right)+\\left(P_{\\text{res\\_gen1}}-P_{\\text{gen1}}\\right) + \\left(P_{\\text{res\\_gen2}}-P_{\\text{gen2}}\\right))$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T13:19:41.654110Z",
     "start_time": "2025-10-17T13:19:41.633017Z"
    }
   },
   "outputs": [],
   "source": [
    "p_dist_slack = (net.res_ext_grid.p_mw.sum() - 0) + (net.res_gen.p_mw.sum() - net.gen.p_mw.sum())\n",
    "Delta_p = p_dist_slack * 1/3\n",
    "print(f\"Power mismatch (results):\\t{p_dist_slack:6.2f}\" + \"\\n\" +\n",
    "      f\"Adjustment per element:\\t\\t{Delta_p:6.2f}\" + \"\\n\" +\n",
    "      f\"Line losses:\\t\\t\\t{net.res_line.pl_mw.sum():6.2f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this example, the difference in active  power production by the 3 power sources is as follows: $$\\varDelta P\\approx 74.78~MW\\cdot \\frac{1}{3}\\approx 24.93~MW$$\n",
    "Each power source must increase its active power injection by approximately $24.93~MW$ to compensate for the losses of the grid ($\\approx$ $7.78 MW$) and the unbalanced dispatch ($P_{\\text{ext\\_grid}}+P_{\\text{gen1}}+P_{\\text{gen2}} - \\sum P_\\text{load} = -67 MW$)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='third_case'></a>\n",
    "## Third example: unequal slack weights"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T13:19:41.765128Z",
     "start_time": "2025-10-17T13:19:41.661120Z"
    }
   },
   "outputs": [],
   "source": [
    "display(heading_no_dist_slack)\n",
    "gen_disp_mw = deepcopy(net.gen.p_mw.sum())\n",
    "\n",
    "runpp(net)\n",
    "\n",
    "print_powers(net)\n",
    "\n",
    "display(heading_dist_slack)\n",
    "\n",
    "net.ext_grid['slack_weight'] = 6\n",
    "net.gen['slack_weight'] = 3\n",
    "\n",
    "runpp(net, distributed_slack=True)\n",
    "\n",
    "print_powers(net)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Sanity check"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T13:19:41.859950Z",
     "start_time": "2025-10-17T13:19:41.838422Z"
    }
   },
   "outputs": [],
   "source": [
    "p_mismatch = net.gen.p_mw.sum() - net.load.p_mw.sum() \n",
    "pl_mw = net.res_line.pl_mw.sum()\n",
    "p_dist_slack = (net.res_ext_grid.p_mw.sum() - 0) + (net.res_gen.p_mw.sum() - net.gen.p_mw.sum()) \n",
    "display(Markdown(f\"Power mismatch (setpoint): ${abs(p_mismatch):.2f}~MW$, line losses: ${pl_mw:.2f}~MW$, total: ${abs(p_mismatch)+pl_mw:.2f}~MW$\"))\n",
    "display(Markdown(f\"Power mismatch (results): ${p_dist_slack:.2f}~MW$\"))\n",
    "Delta_p_6_12 = p_dist_slack * 6/12\n",
    "Delta_p_3_12 = p_dist_slack * 3/12\n",
    "display(Markdown(\"$\\\\varDelta P_{ext\\_grid}= P_{\\\\text{dist\\_slack}} \\\\cdot \\\\frac{6}{6+3+3}=%.2f~MW$\" % Delta_p_6_12))\n",
    "display(Markdown(\"$\\\\varDelta P_{gen}= P_{\\\\text{dist\\_slack}} \\\\cdot \\\\frac{3}{6+3+3}=%.2f~MW$\" % Delta_p_3_12))\n",
    "display(Markdown(\"$\\\\varDelta P = 1 * \\\\varDelta P_{ext\\_grid} + 2 * \\\\varDelta P_{gen}=%.2f~MW$\" % p_dist_slack))"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
